本文作为《Game Programming Pattern》(游戏编程原理)的摘要总结,记录为期一个月的阅读感想与心得体会。
撰写本文时,希望可以尽量遵循star法则,此后的文章同样以这四个要点作为大纲。
situation(情境)
task(任务)
action(行为)
result(结果)
本文代码尽量以unity c#进行演示(最近看的都是cpp代码,时常有被带偏的情况,本人实在太菜菜子了)
类型对象
通过创建一个类来支持新类型的灵活创建,其每个实例都代表了一个不同的对象类型。
我们传统的编程可能是一个monster基类,然后有诸如dragon,troll,witch这样的子类去继承他们。这样会导致几乎所有的时间都在编写简单且几乎相同的子类,而且每次都需要重新编译。
situation
我们需要一个无需重新编译就能修改属性的模式,最好能在无程序员介入的情况下让设计师也能调整。
当需要定义一系列不同种类的对象,又不希望把种类硬编码到类型系统时,本模式很适用。
特别是不知道将来会有什么新的类型(诸如游戏更新,资源包下载),
或者是需要不重新编译或修改代码情况下修改或添加新的类型时,此模式特别适用。
task
我们的任务很简单,游戏中有许多不同的怪物,我们想让他们共享一些特性。当然了他最好可以不需要重新编译就能支持修改。
action
我们重构我们的代码,使得每一种怪物都“has a”种类。我们仅声明单个Monster类和单个Breed类,而不是从Monster派生出各个种类。
class Breed
{
public:
Breed(int health_,const char* _attack)
:health(health_),attack(_attack){}
Get(){...}
private:
int health;
const char* attack;
}
我们创建了一个包含两个数据字段的容器,让我们看看monster如何使用它
class Monster
{
public:
Monster(Breed& breed_)
:breed(breed_),health(breed_.GetHealth())
const char* GetAttack()
{
return breed.GetAttack();
}
private:
int health;
Breed& breed;
}
我们构造一个怪物时,给他一个种族对象的印象,由此来取代之前的派生关系。在构造函数中,怪物使用种族对象来获取他的初始生命值,其他所需的也只需要调取他所属Breed的方法。这是这个模式的核心思想。
我们成功的将一部分数据从硬编码的类继承中解放了出来,成为了可在运行时定义的数据。
我们最终会有成百上千个种类,我们可以仿照多个怪物子类通过类型的基类来共享特性一样,我们直接让种族之间共享特性。我们通过派生来实现,只不过不采用语言层面上的派生,而是自己实现。
class Breed
{
public:
Breed(Breed* parent_,int health_,const char* _attack)
:parent(parent_),health(health_),attack(_attack){}
int GetHealth()
{
if(health!=0||parent=NULL)
{
return health;
}
else
{
return parent.GetHealth();
}
}
private:
Breed* parent;
int health;
const char* attack;
}
这样做即使在运行时修改种类,他也能正常运行。另一方面会占用更多内存,而且更慢,这方面值得好好权衡利弊。如果能保证基类不变,可以直接在构造函数里面将基类的值拷贝过来,这样会更快些。
上述过程中,我们是相当于是先分配了一段空内存,然后给他赋予了类型。我们希望能调用类自身的构造函数,由它为我们创造新的实例。
class Breed
{
public:
Monster* NewMonster()
{
return new Monster(*this);
}
}
class Monster
{
friend class Breed;
public:
const char* GetAttack()
{
return breed.GetAttack();
}
private:
Monster(Breed& breed_)
:breed(breed_),health(breed_.GetHealth())
int health;
Breed& breed;
}
我们创建怪物由new Monster(anyBreed)变成了anyBreed.newMonster()
我们将构造方法设为私有,同时将Breed设为友元类,意味着Breed仍然能访问到这个构造方法,newMonster成为了创建怪物的唯一方法。
初始化一般在内存分配后,我们提前获得了用于储存它的内存。
在Breed里定义一个构造函数,能让我们在控制权被移交到初始化函数前,从一个池或者自定义的堆里面获取内存,我们能因此自己控制对象在内存中存在的位置和时间。
result
类型模式让我们像设计自己的语言一样设计我们的系统,但时间开销是不可忽略的。
我们成功做到了不同的对象之间能共享数据,另一个角度上解决这个问题的是原型模式。
享元模式也很接近,但享元模式更倾向节约内存,而类型对象重点在于灵活性。
这个模式和状态模式相同,都把对象的部分定义工作交给了另一个代理对象来实现。但这个模式的代理对象往往是一些静态的内容,而状态模式的代理对象则是描述对象当前状态的临时数据。
组件模式
允许一个单一的实体跨越多个不同域而不会导致耦合
软件设计的趋势是尽可能多的使用组合而不是继承,为了两个类之间代码共享他们应该拥有同一个类,而不是继承同一个类。
situation
组件模式最常见于游戏中定义实体的核心类,当我们有一个设计多个领域的类(物理,渲染,声音时),我们希望这些领域能保持解耦。
又或者是希望定义很多共享不同能力的对象,但采用继承的方法很难精准无误的重用代码。
这个时候,我们可能就需要组件模式。
task
我们需要将代码解耦,将一个大类里面的各个领域划分成独立的部分,然后让类持有他。这个类本身成为这些组件的容器。
action
让我们看看原先庞大的类吧
class Bjorn
{
public:
Bjorn():velocity_(0),x_(0),y_(0);
void update(World &world,Graphics& graphics)
{
//允许用户输入英雄的速度
switch(Controller::getJoystickDirection())
{
case DIR_LEFT:
velocity_-=WALK_ACCECLERATION;
break;
case DIR_RIGHT:
velocity_+=WALK_ACCECLERATION;
break;
}
//通过速度修改位置
x_+=velocity_;
world.resovleCollision(volume_,x_,y_,volume_);
//绘出恰当的精灵
Sprite* sprite=&spriteStand_;
if(velocity_<0)sprite=&spriteWalkLeft;
else if(velocity_>0)sprite=&priteWalkRight;
graphics.draw(sprite,x_,y_);
}
private:
static const int WALK_ACCECLERATION=1;
int velocity_;
int x_,y_;
Volume volume_;
Sprite spriteStand_;
Sprite spriteWalkLeft;
Sprites priteWalkRight;
}
在这个类中,我们通过操控杆的输出来判定如何对主角进行加速,通过物理引擎确定新的位置,最后将主角绘制到屏幕上。
我们慢慢的将主角分割成独立的域,先从输入开始。
输入域所做的事情是读入用户的输入并调整自身速度。
让我们将这个逻辑封装到一个独立的类:
class Input
{
public:
void update(Bjorn& bjorn)
{
switch(Controller::getJoystickDirection())
{
case DIR_LEFT:
bjorn.velocity_-=WALK_ACCECLERATION;
break;
case DIR_RIGHT:
bjorn.velocity_+=WALK_ACCECLERATION;
break;
}
}
private:
static const int WALK_ACCECLERATION=1;
}
对物理部分也做出同样的工作:
class Physics
{
public:
void update(Bjorn& bjorn,World& world)
{
bjorn.x_+=velocity_;
world.resovleCollision(volume_,x_,y_,volume_);
}
private:
Volume volume_;
}
此时volume_由物理组件持有了。
最后是一样很重要的渲染代码:
class Graphics
{
public:
void update(Bjorn& bjorn,Graphcis& graphcis)
{
Sprite* sprite=&spriteStand_;
if(bjorn.velocity_<0)sprite=&spriteWalkLeft;
else if(bjorn.velocity_>0)
sprite=&priteWalkRight;
graphics.draw(sprite,x_,y_);
}
private:
Sprite spriteStand_;
Sprite spriteWalkLeft;
Sprites priteWalkRight;
}
这样一来,我们几乎将所有东西都分隔开来了。只剩下没有多少代码的主角:
class Bjorn
{
public:
int velocity;
int x,y;
void update(World &world,Graphics& graphics)
{
input.update(*this);
Physics.update(*this,world);
Graphics.update(*this,graphics);
}
private:
Input input;
Physics physics;
Graphics graphics;
}